package com.ptppay.cf.starter;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import org.apache.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Peer;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.listeners.OnTransactionBroadcastListener;
import org.bitcoinj.script.Script;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.wallet.UnreadableWalletException;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.WalletExtension;
import org.bitcoinj.wallet.WalletFiles;
import org.bitcoinj.wallet.listeners.KeyChainEventListener;
import org.bitcoinj.wallet.listeners.WalletChangeEventListener;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.protobuf.ByteString;
import com.ptppay.cf.common.CommonEnums;
import com.ptppay.cf.core.business.service.CFService;
import com.ptppay.cf.domain.CoinPrice;
import com.ptppay.cf.processor.mq.MqPublisher;

import cn.finder.bitcoin.AddWalletEventListener;
import cn.finder.bitcoin.Btc;
import cn.finder.bitcoin.BtcTools;
import cn.finder.bitcoin.DownloadBlockChainComplete;
import cn.finder.httpcommons.utils.JsonUtils;
import cn.finder.mq.router.DefaultTopicRouterMqttClient;
import cn.finder.wae.common.comm.AESV3;
import cn.finder.wae.common.comm.AppApplicationContextUtil;
import cn.finder.wae.helper.EmailHelper;

/***
 * Btc Chain
 * 
 * @author finder
 *
 * 
 *
 */


public class BtcListenerServletV2 extends HttpServlet {

	private static Logger logger = Logger.getLogger(BtcListenerServletV2.class);

	/**
	 * 
	 */
	private static final long serialVersionUID = -7958000580578197415L;

	
	private int LEAST_CONFIRMS=1; // 1 confirms
	
	CFService cFService;
	DefaultTopicRouterMqttClient mqttClient;
	
	
	@Override
	public void init(ServletConfig servletConfig) throws ServletException {

		String enabled = servletConfig.getInitParameter("enabled");

		if ("true".equalsIgnoreCase(enabled)) {
			logger.info("BtcListenerServletV2.init  starting....  ");
			cFService =AppApplicationContextUtil.getContext().getBean("cFService", CFService.class);
			
			mqttClient =AppApplicationContextUtil.getContext().getBean("finderMqttClient", DefaultTopicRouterMqttClient.class);
			
			
			
			new BitCoinReceiver().start();
		}


		
		
		
		
	}

	@Override
	public void destroy() {
		
		//save wallet
	    List<Wallet> wallets=Btc.get().getWallets();
	    
	    for(Wallet wallet:wallets){
	    	try{
	    		wallet.shutdownAutosaveAndWait();
	    	}catch(Throwable e){
	    		logger.error(e);
	    	}
	    }
		
		Btc.get().stop();
	}
	
	
	/***
	 * save chain tx , not need. we can find tx detail to block info view
	 * @param trans
	 */
	/*private void saveTxInfo(Map<String,Object>){
		
		Map<String,Object> txInfo=new HashMap<>();
		txInfo.put("tx_hash", trans.getHash());
		txInfo.put("from_address", trans.getFrom());
		txInfo.put("to_address", trans.getTo());
		//txInfo.put("amt", trans.getValue());
		//txInfo.put("block_number", trans.getBlockNumber());
		txInfo.put("block_number", "");
		//txInfo.put("creates", trans.getCreates());
		txInfo.put("creates", "");
		txInfo.put("gas", trans.getGas());
		txInfo.put("gas_price", trans.getGasPrice());
		txInfo.put("gas_price_raw", trans.getGasPriceRaw());
		txInfo.put("gas_raw", trans.getGasRaw());
		txInfo.put("input", trans.getInput());
		txInfo.put("nonce", trans.getNonce());
		txInfo.put("nonce_raw", trans.getNonceRaw());
		//txInfo.put("publick_key", trans.getPublicKey());
		txInfo.put("publick_key", "");
		txInfo.put("r", trans.getR());
		//txInfo.put("raw", trans.getRaw());
		txInfo.put("raw", "");
		txInfo.put("s", trans.getS());
		txInfo.put("transaction_index", trans.getTransactionIndex());
		txInfo.put("value", trans.getValue());
		txInfo.put("value_raw", trans.getValueRaw());
		txInfo.put("token_type", CommonEnums.LABOR_COIN_TYPE.getValue());
		txInfo.put("tx_fee", trans.getGas().multiply(trans.getGasPrice()));
		
		txInfo.put("has_error", eth_trans.hasError()?"1":"0");
		if(eth_trans.hasError()){
			txInfo.put("error_code", eth_trans.getError().getCode());
			txInfo.put("error_data", eth_trans.getError().getData());
			txInfo.put("error_message", eth_trans.getError().getMessage());
		}
		txInfo.put("status", CommonEnums.TX_STATUS_COMPLETE.getValue());
		
		cFService.saveTxInfo(txInfo);
	}*/
	
	public static class BitCoinReceiver extends Thread implements WalletCoinsReceivedEventListener,OnTransactionBroadcastListener,WalletChangeEventListener,WalletFiles.Listener,DownloadBlockChainComplete,KeyChainEventListener,AddWalletEventListener {
		
		
		CFService cFService;
		public BitCoinReceiver(){
			
			cFService =AppApplicationContextUtil.getContext().getBean("cFService", CFService.class);
		}
		
		@Override
		public void run() {
			try {
				init();

			} catch (Exception  e) {
				logger.error(e);
			} 

		}

		private void init() throws BlockStoreException, UnreadableWalletException {
			
			Btc.get().setLogLevel(Btc.LogLevel.DEFAULT);
			
			Btc.get().setDownloadBlockChainComplete(this);
			Btc.get().setOnTransactionBroadcastListener(this);
			Btc.get().setAddWalletEventListener(this);
			loadWallets();
			
			Btc.get().start(true);
			
		}

		
		@Override
		public void onTransaction(Peer peer, Transaction tx) {
			// TODO Auto-generated method stub

			logger.info("============receive broadcast=========");
			logger.info(tx);
			
			logger.info(" number of broadcast peers:"+tx.getConfidence().numBroadcastPeers());
			logger.info(" DepthInBlocks:"+tx.getConfidence().getDepthInBlocks());
			logger.info(" confidence type:"+ tx.getConfidence().getConfidenceType());
			
			
			logger.info("============inputs=========");
			
			logger.info(tx.getInputs().size() +" INPUTS");
			

			logger.info("============outputs=========");

			logger.info(tx.getOutputs());

			logger.info("============output detail=========");
			// UTXO
			List<TransactionOutput> tx_outs = tx.getOutputs();

			for (TransactionOutput out_item : tx_outs) {

				Address address = null;
				try {
					if(out_item.getScriptPubKey().getScriptType()==Script.ScriptType.P2SH){
						
						//P2SH: back to sender address  (pay-to-script-hash)
						address = out_item.getAddressFromP2SH(Btc.get().getNetworkParameters());
						
						logger.info("P2SH=>"+address.toBase58() + "(" + out_item.getValue().toFriendlyString() + ")");
						
						
					}else if(out_item.getScriptPubKey().getScriptType()==Script.ScriptType.P2PKH){
						
						//P2PK receiver address coin number 
						address =out_item.getAddressFromP2PKHScript(Btc.get().getNetworkParameters());
						
						logger.info(address.toBase58() + "(" + out_item.getValue().toFriendlyString() + ")");
						
						double value=out_item.getValue().longValue()/100000000.0;
						
						String to=address.toBase58();
						// search bind equipment
						
						
					    Map<String,Object>	bindItem=cFService.findBindingByWalletAddressByMchAddress(to);
						//
						 if(bindItem!=null){
							 
							 
							 //计算金额 
							// BigInteger tx_fee;
						//	BigInteger tx_fee= tx.getGas().multiply(tx.getGasPrice());//0.946
							
							String eqt_id=bindItem.get("eqt_id").toString();
							
							//compare to pretx
							Map<String,Object> pretx=cFService.findLatestingPreTxInfo(eqt_id, to, CommonEnums.PRETXS_BUY_SELL_SELL.getValue());
							
							if(pretx!=null){
								//tx is exists
								double pretx_money_amt=Double.parseDouble(pretx.get("money_amt").toString());
								// judgement coin account
								double need_coin_amt=Double.parseDouble(pretx.get("coin_amt").toString());
								
								//BTC:RMB=1:5000
								//double exchange_rate=Double.parseDouble(cFService.findEqtParam(eqt_code, "lab").get("valuestr").toString());
								String coin_type="";
								if(to.startsWith("1")){
					    			   // btc mainnet
					    			   coin_type=CommonEnums.COIN_TYPE_BTC.getValue();
					    			} else if(to.startsWith("2") || to.startsWith("m") ||to.startsWith("n")){
					    			   // btc testnet
					    			   coin_type=CommonEnums.COIN_TYPE_BTCT.getValue();
					    			}
								
								
								String eqt_code=cFService.findEquipmentCode(eqt_id);
								CoinPrice coinPrice=cFService.findCoinPrice(eqt_code, coin_type);
								double exchange_rate=coinPrice.getSell_price();// receive coin price
								
								double miner_fee=Double.parseDouble(coinPrice.getMiner_fee().toString());
								
								
								
								
								//update chain tx to pretx
								
								String pretx_id=pretx.get("id").toString();
								
								cFService.updatePreTxByChainTx(pretx_id, tx.getHash().toString(), value+"");
								
								//save chain txinfo
								
								// not need
								//saveTxInfo(tx);
								
								if(value<need_coin_amt){
									// act_coin amt letter  need coin amt
									
									//转的太多
									Double money_amt=value*exchange_rate;
									//去整数
									int money=money_amt.intValue();
									Map<String,Object> returnData=new HashMap<String,Object>();
									returnData.put("eqt_code", eqt_code);
									returnData.put("pre_tx_id", pretx_id);
									returnData.put("money_amt", pretx_money_amt);
									returnData.put("money_type", pretx.get("money_type"));
									returnData.put("status", "L");
									returnData.put("msg", "outcash.transfer.coin.is.less");
									sendToClient(eqt_code,returnData);
								}else if(value>=need_coin_amt){
									
									BigDecimal bg = new BigDecimal(value-need_coin_amt);  
						            double diff_coin = bg.setScale(8, BigDecimal.ROUND_HALF_UP).doubleValue();
									
									if(diff_coin>miner_fee){
										//转的太多  多与的打印交易
										Double money_amt=need_coin_amt*exchange_rate;
										//去整数
										int money=money_amt.intValue();
										
										Map<String,Object> returnData=new HashMap<String,Object>();
										returnData.put("eqt_code", eqt_code);
										returnData.put("pre_tx_id", pretx_id);
										returnData.put("money_amt", pretx_money_amt);
										returnData.put("money_type", pretx.get("money_type"));
										returnData.put("status", "B");
										returnData.put("msg", "outcash.transfer.coin.is.more");
										returnData.put("more_money_amt", (value-need_coin_amt));
										// 设备里面缺钱 应该也需要通知设备
										sendToClient(eqt_code,returnData);
									}else{
										
										//出钞金额 (元)
										
										Double money_amt=need_coin_amt*exchange_rate;
										//去整数
										int money=money_amt.intValue();
										//是否还需要判断 准备出钞金额 和 提交金额
										
										
										Map<String,Object> returnMap=new HashMap<String,Object>();
										returnMap.put("eqt_code", eqt_code);
										returnMap.put("pre_tx_id", pretx_id);
										returnMap.put("money_amt", pretx_money_amt);
										returnMap.put("money_type", pretx.get("money_type"));
										returnMap.put("status", "C");
										returnMap.put("msg", "outcash.normal");
										// 设备里面缺钱 应该也需要通知设备
										sendToClient(eqt_code,returnMap);
									}
									
									
								}
								
								
								
								
								
								
								
								
								
							}
							
							
							
						 }
						
						
					}else if(out_item.getScriptPubKey().getScriptType()==Script.ScriptType.PUB_KEY){
						String pub_key=new String(out_item.getScriptPubKey().getPubKeyHash());
						logger.info(" pub key: "+pub_key);
					}
					
					
					
				} catch (Exception e) {
					System.err.println(e.toString());
				}
				

			}
			
			logger.info("");

		}
		
		
		@Override
		public synchronized void onCoinsReceived(final Wallet wallet, final Transaction transaction, Coin prevBalance, Coin newBalance) {
			final Coin value = transaction.getValueSentToMe(wallet);
			
			logger.info(" transaction "+transaction.getHashAsString());
			
			logger.info("Received tx for " + value.toFriendlyString() + ": " + transaction);
			
			logger.info("Previous balance is " + prevBalance.toFriendlyString());

			logger.info("New estimated balance is " + newBalance.toFriendlyString());

			logger.info("Coin received, wallet balance is :" + wallet.getBalance());
			
			
		/*	logger.info(String.format(" number of broadcast peers: %s , confidence type:%s, block height :%s, number of confirms:%s", 
					transaction.getConfidence().numBroadcastPeers()+"",
					transaction.getConfidence().getConfidenceType(),
					transaction.getConfidence().getAppearedAtChainHeight(),
					transaction.getConfidence().getDepthInBlocks()));*/
			try{
				saveNow(wallet);
			}
			catch(Exception e){
				
			}
			try{
				cFService.updateTxConfirms(transaction.getHashAsString(),BtcTools.getConfirms(transaction));
			}catch(Exception e){
				
			}
			
			
			//confirms
			int CONFIRMS=1;
			
			Futures.addCallback(transaction.getConfidence().getDepthFuture(CONFIRMS), new FutureCallback<TransactionConfidence>() {
				public void onSuccess(TransactionConfidence result) {
					// Transaction confirmed
					logger.info("Transaction confirmed, wallet balance is :" + wallet.getBalance());
					//update local transaction status
					//  transaction.getHashAsString()
					//  transaction.getValue(wallet).value
					
					
				}

				public void onFailure(Throwable t) {
					t.printStackTrace();
				}
			});
		}

		private void saveNow(Wallet wallet){
			logger.info("===wallet changed ,now balance:"+wallet.getBalance());
			String wallet_id= wallet.getTag("wallet_id").toStringUtf8();
			
			String walletDir=new File(Btc.get().getChainFile()).getParent();
			String walletFileName=BtcTools.buildDefaultWalletName(Btc.get().getNetworkParameters(),wallet_id);
			File f=new File(walletDir+File.separator+walletFileName);
			try {
				wallet.saveToFile(f);
			} catch (IOException e) {
				logger.error(e);
			}
		}
		
		
		@Override
		public void onWalletChanged(Wallet wallet) {
			saveNow(wallet);
		}

		@Override
		public void onBeforeAutoSave(File tempFile) {
			logger.info("==onBeforeAutoSave:"+tempFile.getPath());
			
		}
		
		@Override
		public void onAfterAutoSave(File newlySavedFile) {
			logger.info("==onAfterAutoSave:"+newlySavedFile.getPath());
			
		}

		@Override
		public void complete() {
			//
			
			
			
			
			
			// 启动监听线程 监听 
			
			
		/*	List<Map<String,Object>> saleCoinWallets= cFService.findCoinSupportWalletInfo(CommonEnums.COIN_TYPE_BTCT.getValue(),CommonEnums.PRETXS_BUY_SELL_SELL.getValue());
			
			List<ECKey> eckeys=new ArrayList<ECKey>();
			for(Map<String,Object> saleCoinWalletItem:saleCoinWallets){
				
				String private_key=saleCoinWalletItem.get("private_key").toString();
				
				ECKey key = BtcTools.getECKeyFromPrivateKey(private_key,Btc.get().getNetworkParameters());
				eckeys.add(key);
				
				logger.info("wallet address: "+key.toAddress(Btc.get().getNetworkParameters()));
			}
			//wallet.importKeys(eckeys);
*/			
			
			
			/*File blockFile = new File("d:\\cf-bitcoin-blocks");
			SPVBlockStore blockStore = new SPVBlockStore(params, blockFile);

			BlockChain blockChain = new BlockChain(params, blockStore);
			
			// if use proxy
			System.setProperty("socksProxyHost", "127.0.0.1");
			System.setProperty("socksProxyPort", "9050");
			peerGroup = new PeerGroup(params, chain, new BlockingClientManager());
			
			PeerGroup peerGroup = new PeerGroup(params, blockChain);
			peerGroup.addPeerDiscovery(new DnsDiscovery(params));
			
			
			peerGroup.addWallet(wallet); //这里可以动态添加 需要管理的钱包权限

			logger.info("Start peer group");
			peerGroup.start();
			
			//ListenableFuture<?> a = peerGroup.startAsync();
			
			logger.info("Downloading block chain");
			
			peerGroup.downloadBlockChain(); //choke
		
			
			
			
			
			logger.info("Block chain downloaded");

			wallet.addCoinsReceivedEventListener(this);

			//
			peerGroup.addOnTransactionBroadcastListener(this);

			logger.info("block height: "+peerGroup.getMostCommonChainHeight());*/
			
			// self define listener must later...
			/*List<Wallet> wallets=Btc.get().getWallets();
			for(Wallet wallet:wallets){
				logger.info(" ===== tag:wallet_id: "+wallet.getTag("wallet_id").toStringUtf8());
				wallet.addCoinsReceivedEventListener(this);
				wallet.addChangeEventListener(this);
				wallet.addKeyChainEventListener(this);
				
			}*/
		}
		
		private void loadWallets(){

			// find merchant
			List<Map<String,Object>> merchants= cFService.findAllMerchant(CommonEnums.MCH_STATUS_A.getValue());
			if(merchants!=null){
				for(Map<String,Object> mch:merchants){
					
					try{
						
					
					String mch_id=mch.get("id").toString();
					
					List<Map<String,Object>> saleCoinWallets=cFService.findMchLinkCoinPriceWalletItems(mch_id, CommonEnums.COIN_TYPE_BTC.getValue(),CommonEnums.PRETXS_BUY_SELL_SELL.getValue());
					
					
					
					List<ECKey> keys=new ArrayList<ECKey>();
					for(Map<String,Object> saleCoinWalletItem:saleCoinWallets){
						
						String private_key=saleCoinWalletItem.get("private_key").toString();
						
						String public_key=saleCoinWalletItem.get("public_key").toString();
						String pwd=saleCoinWalletItem.get("pwd").toString();
						try {
							private_key=new String(AESV3.decrypt(private_key,pwd,public_key));
							
							ECKey key = BtcTools.getECKeyFromPrivateKey(private_key,Btc.get().getNetworkParameters());
							keys.add(key);
							
							logger.info("wallet address: "+key.toAddress(Btc.get().getNetworkParameters()));
							
						} catch (Exception e) {
							e.printStackTrace();
						}
						
						
						
						
					}
					
					
					//BtcTools.createEmptyWallet(Btc.get().getNetworkParameters());
					// load wallet file
					
					String walletDir=new File(Btc.get().getChainFile()).getParent();
					String walletFileName=BtcTools.buildDefaultWalletName(Btc.get().getNetworkParameters(),mch_id);
							
					File f=new File(walletDir+File.separator+walletFileName);
					Wallet wallet=null;
					if(f.exists()){
						//load from file
						try{
							//file perhaps spoil 
							wallet=Wallet.loadFromFile(f, (WalletExtension[])null);
							if(wallet==null){
								wallet=new Wallet(Btc.get().getNetworkParameters());
							}
						}
						catch(Exception e){
							wallet=new Wallet(Btc.get().getNetworkParameters());
						}
						
					}else {
						// create wallet
						wallet=new Wallet(Btc.get().getNetworkParameters());
						try {
							wallet.saveToFile(f);
						} catch (IOException e) {
							logger.error(e);
						}
					}
					try {
						wallet.saveToFile(f);
					} catch (IOException e) {
						logger.error(e);
					}
					
					wallet.autosaveToFile(f, 5000, TimeUnit.SECONDS, this);
					
					wallet.importKeys(keys);
					
					
					
					wallet.setTag("wallet_id",ByteString.copyFromUtf8(mch_id));//save data to wallet file
					Btc.get().addWallet(wallet);
					
					
					logger.info(" mch ["+ mch_id +"] wallet balance:"+wallet.getBalance().toFriendlyString());
					
					}
					catch(Exception e){
						
					}
				}
			}
			
		}

		@Override
		public void onKeysAdded(List<ECKey> keys) {
			// TODO Auto-generated method stub
			
		}

		@Override
		public void onAddWallet(Wallet wallet) {
			wallet.addCoinsReceivedEventListener(this);
			wallet.addChangeEventListener(this);
			wallet.addKeyChainEventListener(this);
			
		}
		
		
		
		
		private void sendToClient(String eqt_code,Map<String,Object> returnMap){
			
			
			// 设备里面缺钱 应该也需要通知设备
			
			
			String returnData=JsonUtils.getJsonString4JavaPOJO(returnMap);
			String topic=MqPublisher.TOPIC_CF_OUTCASH;
			try {
				//mqttClient.publishMsgOne(topic, returnData, eqt_code);
				MqPublisher.publish(topic, returnMap, eqt_code);
				
			} catch (Throwable e) {
				
				//记录到日志 ，并且发邮件
				new EmailHelper().sendEmailToAdminInThread(String.format(" MQ publish exception .topic: %s,payload:%,to_client_id:%s, exception detail:%s", topic,returnData,eqt_code,e.toString()), e.toString());
				
			} 
		}

	}
	


}
